<template>
	<div class="default-main">

		<cus-form id="productionLine-form-01" collapsable icon="local-cus-ruleEngine" label="产线规划">
			<template #title-after>
				<el-button type="primary" @click.stop="calculate">
					<template #icon>
						<icon name="el-icon-Search"></icon>
					</template>
					<el-text style="color: var(--el-text-color-primary)">计算产线</el-text>
				</el-button>
				<el-button icon="el-icon-Refresh" size="small" type="primary" @click.stop="resetLine">
					重置
				</el-button>
			</template>
			<template #form>
				<el-form ref="formRef" :model="params" :rules="rules" label-width="auto" scroll-to-error>
					<el-row :gutter="20" justify="start">
						<el-col :span="8">
							<el-form-item label="产物" prop="selectedItem">
								<el-select v-model="params.selectedItem" clearable filterable placeholder="模糊查询" value-key="id">
									<el-option v-for="item in itemOptionList" :key="item.id" :label="item.itemName!" :value="item">
										<div style="display: flex;justify-content: space-between">
											<div>{{ item.itemName }}</div>
											<div v-if="item.image">
												<el-image :src="getImage(item.image)" fit="contain" style="width: 30px; height: 30px"></el-image>
											</div>
										</div>
									</el-option>
								</el-select>
							</el-form-item>
						</el-col>
						<el-col :span="8">
							<el-form-item label="产出速度（/min）" prop="quantityPerMinute">
								<el-input-number v-model="params.quantityPerMinute" :max="6000" :min="0" :step="0.0001" class="w100" controls-position="right" step-strictly/>
							</el-form-item>
						</el-col>
						<el-col :span="8">
							<el-form-item label="默认采矿机" prop="defaultMiner">
								<business-code-select v-model="params.defaultMiner" :filterable="false" code-type="defaultMiner" placeholder="请选择"/>
							</el-form-item>
						</el-col>
					</el-row>
					<el-row :gutter="20" justify="start">
						<el-col :span="8">
							<el-form-item prop="defaultExpandAll">
								<template #label>
									<span>自动计算下级产线</span>
									<el-tooltip :hide-after="0" content="如果遇到无限加载，请关闭此项" placement="bottom">
										<icon name="fa fa-question-circle" style="color: var(--el-text-color-primary)"></icon>
									</el-tooltip>
								</template>
								<el-switch v-model="params.defaultExpandAll"></el-switch>
							</el-form-item>
						</el-col>
						<el-col :span="8">
							<el-form-item prop="defaultOverclock">
								<template #label>
									<span>默认超频倍率</span>
									<el-tooltip :hide-after="0" content="仅对于不同产物首次计算产线时有效" placement="bottom">
										<icon name="fa fa-question-circle" style="color: var(--el-text-color-primary)"></icon>
									</el-tooltip>
								</template>
								<el-slider v-model="params.defaultOverclock" :marks="OVERCLOCK_PERCENTAGE_MARKS" :max="2.5" :min="0.01" :step="0.01" show-input></el-slider>
							</el-form-item>
						</el-col>
					</el-row>
				</el-form>
			</template>
		</cus-form>

		<cus-form id="productionLine-form-02" collapsable icon="local-cus-ruleEngine" label="建筑取整优化">
			<template #form>
				<el-form ref="optimizeFormRef" :model="optimizeParams" label-width="auto">
					<el-row :gutter="20" justify="space-between">
						<el-col :span="8">
							<el-form-item>
								<template #label>
									<span>资源采集建筑</span>
									<el-tooltip :content="`资源采集建筑（本质上指的是不需要原料就可以生产的建筑）是否需要取整，包括：${ResourceCollectorNames.join('，')}`"
									            :hide-after="0" placement="bottom">
										<icon name="fa fa-question-circle" style="color: var(--el-text-color-primary)"></icon>
									</el-tooltip>
								</template>
								<el-switch v-model="optimizeParams.considerMiner" @change="judgeOptimization"></el-switch>
							</el-form-item>
						</el-col>
						<el-col :span="16">
							<el-form-item>
								<template #label><span v-if="optimizationTip">优化措施</span></template>
								<el-text :type="needOptimization ? 'warning' : 'success'">{{ optimizationTip }}</el-text>
								<el-button v-if="needOptimization" size="small" style="margin-left: 20px" type="primary" @click="optimize">
									<template #icon>
										<icon name="local-cus-calculate"></icon>
									</template>
									应用优化
								</el-button>
							</el-form-item>
						</el-col>
					</el-row>
				</el-form>
			</template>
		</cus-form>

		<div v-if="lineVisible">
			<!-- 产线图按钮区 -->
			<cus-table-title id="productionLine-table-title" label="产线">
				<template #after>
					<el-space>
						<el-button size="small" style="margin-left: 20px" type="primary" @click="showStatistics">
							<template #icon>
								<icon name="el-icon-DataAnalysis"></icon>
							</template>
							数据统计
						</el-button>
						<el-button-group size="small">
							<el-button type="primary" @click="toggleDirection">
								<template #icon>
									<icon name="local-aliIntl-document-conversion"/>
								</template>
								图表方向：{{ diagramDirection === 'horizontal' ? '横向' : '纵向' }}
							</el-button>
							<el-button type="primary" @click="diagram?.zoomToFit()">
								<template #icon>
									<icon name="local-cus-exit-fullscreen"></icon>
								</template>
								显示全部
							</el-button>
						</el-button-group>
						<el-button-group size="small">
							<el-tooltip :disabled="!isVisitor()" :hide-after="0" :show-after="500" content="游客模式下，产线收藏功能不可用，请注册账号后使用~" effect="dark"
							            placement="top">
								<el-button v-auth="'g-user-production:saveUserProduction'" :disabled="isVisitor()" :loading="favorButtonLoading" type="primary"
								           @click="favor">
									<template #icon>
										<icon name="local-aliIntl-favorites-fill"></icon>
									</template>
									收藏
								</el-button>
							</el-tooltip>
							<el-button type="primary" @click="saveAsImage">
								<template #icon>
									<icon name="fa fa-download"></icon>
								</template>
								保存为图片
							</el-button>
						</el-button-group>
						<el-button size="small" type="primary" @click="showInstructions">
							<template #icon>
								<icon name="fa fa-question-circle"></icon>
							</template>
							说明
						</el-button>
					</el-space>
				</template>
			</cus-table-title>

			<!-- 产线图 -->
			<div v-loading="myDiagramLoading">
				<div id="productionLineDiagram" :style="{minHeight: diagramMaxHeight + 'px'}" class="production-line-diagram"></div>
			</div>
		</div>

		<!-- 抽屉（更换配方） -->
		<el-drawer v-model="drawerVisible.chooseRecipe" :show-close="false" append-to-body direction="btt" size="50%">
			<template #header>
				<el-text size="large" type="info">更换配方</el-text>
				<el-radio-group v-model="recipeReplaceType">
					<el-radio border value="current">仅当前节点</el-radio>
					<el-radio border value="all">所有同类配方</el-radio>
				</el-radio-group>
			</template>
			<el-table :data="recipeTableData" border fit highlight-current-row>
				<el-table-column align="center" label="选取" width="60">
					<template v-slot="scope">
						<el-radio-group v-model="selectedRecipeId" @change="selectedRecipeChanged">
							<el-radio :value="scope.row.recipeId"></el-radio>
						</el-radio-group>
					</template>
				</el-table-column>
				<el-table-column align="center" label="序号" type="index" width="60"></el-table-column>
				<el-table-column align="center" label="配方" min-width="150">
					<template v-slot="scope">
						{{ scope.row.recipeNameDisplayed }}
					</template>
				</el-table-column>
				<el-table-column align="center" label="原料" min-width="150">
					<template v-slot="scope">
						<p v-for="materialVo in scope.row.materialList" class="cus-center">{{
								materialVo.quantityPerProduction + '*'
							}}
							<el-link :underline="false" type="primary" @click="showItemDetail(materialVo.itemId)">
								{{ materialVo.itemName }}
							</el-link>
							{{ '（' + materialVo.quantityPerMinute + '/min）' }}
							<el-image v-if="materialVo.image" :src="getImage(materialVo.image)" fit="contain"
							          style="width: 50px; height: 50px; cursor: pointer" @click="showItemDetail(materialVo.itemId)"
							></el-image>
						</p>
					</template>
				</el-table-column>
				<el-table-column align="center" label="产物" min-width="150">
					<template v-slot="scope">
						<p v-for="productVo in scope.row.productList" class="cus-center">
							{{ productVo.quantityPerProduction + '*' }}
							<el-link :underline="false" type="primary" @click="showItemDetail(productVo.itemId)">{{
									productVo.itemName
								}}
							</el-link>
							{{ '（' + productVo.quantityPerMinute + '/min）' }}
							<el-image v-if="productVo.image" :src="getImage(productVo.image)" fit="contain"
							          style="width: 50px; height: 50px; cursor: pointer" @click="showItemDetail(productVo.itemId)"
							></el-image>
						</p>
					</template>
				</el-table-column>
				<el-table-column align="center" label="生产建筑" min-width="150">
					<template v-slot="scope">
						<p v-if="scope.row.buildingVo" class="cus-center">
							<el-link :underline="false" type="primary" @click="showBuildingDetail(scope.row.buildingVo.buildingId)">{{
									scope.row.buildingVo.buildingName
								}}
							</el-link>
							{{
								scope.row.buildingVo.count ? '*' + scope.row.buildingVo.count + '（' + scope.row.buildingVo.countExact + '）' : ''
							}}
							{{
								(scope.row.buildingVo.needPower === 1 ? '（' + scope.row.buildingVo.power + 'MW' + (scope.row.buildingVo.overclockable === 1 ? '，可超频' : '，不可超频') + '）' : '')
							}}
							<el-image v-if="scope.row.buildingVo.image" :src="getImage(scope.row.buildingVo.image)"
							          fit="contain"
							          style="width: 100px; height: 100px; cursor: pointer"
							          @click="showBuildingDetail(scope.row.buildingVo.buildingId)"
							></el-image>
						</p>
						<div v-if="scope.row.handmadeBuildingVoList && scope.row.handmadeBuildingVoList.length > 0">
							<p v-for="building in scope.row.handmadeBuildingVoList" class="cus-center">
								<el-link :underline="false" type="primary" @click="showBuildingDetail(building.buildingId)">
									{{ building.buildingName }}
								</el-link>
								<el-image v-if="building.image"
								          :src="getImage(building.image)"
								          fit="contain"
								          style="width: 100px; height: 100px; cursor: pointer"
								          @click="showBuildingDetail(building.buildingId)"
								></el-image>
							</p>
						</div>
					</template>
				</el-table-column>
			</el-table>
		</el-drawer>

		<!-- 抽屉（数据统计） -->
		<el-drawer v-model="drawerVisible.statistics" :show-close="false" append-to-body direction="rtl" size="35%">
			<template #header>
				<el-text style="font-size: var(--el-font-size-large); font-weight: bold">数据统计</el-text>
			</template>
			<div class="statistics">
				<el-collapse v-model="activeNames">
					<el-collapse-item v-if="basicMaterialList.length > 0" name="material" title="原料">
						<el-descriptions :column="1" border>
							<el-descriptions-item v-for="item in basicMaterialList" :key="item.itemId" label-align="center">
								<template #label>
									{{ item.itemName }}
								</template>
								<div class="cus-center">
									{{ item.quantityPerMinute }}/min
									<el-image :src="getImage(item.image!)" fit="contain"
									          style="width: 50px; height: 50px;cursor: pointer;margin-left: 10px"
									          @click="showItemDetail(item.itemId!)"></el-image>
								</div>
							</el-descriptions-item>
						</el-descriptions>
					</el-collapse-item>
					<el-collapse-item v-if="byProductList.length > 0" name="byProduct" title="副产物">
						<el-descriptions :column="1" border>
							<el-descriptions-item v-for="byProduct in byProductList" :key="byProduct.itemId" label-align="center">
								<template #label>
									<span :style="{'text-decoration': byProduct.extra <= 0 ? 'line-through' : undefined, color: byProduct.extra <= 0 ? 'gray' : undefined}">{{
											byProduct.itemName
										}}</span>
								</template>
								<div class="cus-center">
									<span :style="{'text-decoration': byProduct.extra <= 0 ? 'line-through' : undefined, color: byProduct.extra <= 0 ? 'gray' : undefined}">{{
											byProduct.quantityPerMinute
										}}/min</span>
									<el-image :src="getImage(byProduct.image!)" fit="contain" style="width: 50px; height: 50px;cursor: pointer;margin-left: 10px"
									          @click="showItemDetail(byProduct.itemId!)"></el-image>
									<span v-if="byProduct.extra !== byProduct.quantityPerMinute && byProduct.extra <= 0">（可作为原料，无剩余）</span>
									<span v-if="byProduct.extra !== byProduct.quantityPerMinute && byProduct.extra > 0">（可作为原料，剩余：{{ byProduct.extra }}/min）</span>
								</div>
							</el-descriptions-item>
						</el-descriptions>
					</el-collapse-item>
					<el-collapse-item v-if="productionLineBuildingList.length > 0" name="productionLineBuilding" title="建筑">
						<el-descriptions :column="1" border>
							<el-descriptions-item v-for="building in productionLineBuildingList" :key="building.buildingId"
							                      label-align="center">
								<template #label>
									{{ building.buildingName }}
								</template>
								<div class="cus-center">
									{{ building.count }}<span v-if="building.count && building.count !== building.countExact">（{{ building.countExact }}）</span>&nbsp;
									<el-image :src="getImage(building.image!)" fit="contain" style="width: 50px; height: 50px;cursor: pointer;margin-left: 10px"
									          @click="showBuildingDetail(building.buildingId!)"></el-image>
								</div>
							</el-descriptions-item>
						</el-descriptions>
					</el-collapse-item>
					<el-collapse-item name="power" title="电力">
						<el-descriptions :column="1" border>
							<el-descriptions-item label-align="center">
								<template #label>
									总耗电量
								</template>
								<div class="cus-center">
									{{ totalPower }} MW
								</div>
							</el-descriptions-item>
						</el-descriptions>
					</el-collapse-item>
					<el-collapse-item v-if="othersList.length > 0" name="others" title="其他">
						<el-descriptions :column="1" border>
							<el-descriptions-item v-for="statisticsVo in othersList" label-align="center">
								<template #label>
									{{ statisticsVo.label }}
								</template>
								<div class="cus-center">
									<span v-if="statisticsVo.prefix">{{ statisticsVo.prefix }}</span>
									{{ statisticsVo.value }}
									<span v-if="statisticsVo.suffix">{{ statisticsVo.suffix }}</span>
									<el-image v-if="statisticsVo.image" :src="getImage(statisticsVo.image!)" fit="contain"
									          style="width: 50px; height: 50px;cursor: pointer;margin-left: 10px"
									          @click="statisticsVo.itemId ? showItemDetail(statisticsVo.itemId!) : showBuildingDetail(statisticsVo.buildingId)"></el-image>
								</div>
							</el-descriptions-item>
						</el-descriptions>
					</el-collapse-item>
				</el-collapse>
			</div>
		</el-drawer>

		<!-- 对话框（物品详情） -->
		<cus-item-dialog v-model:visible="dialogVisible.item" :item-id="itemIdDisplayed"/>

		<!-- 对话框（建筑详情） -->
		<cus-building-dialog v-model:visible="dialogVisible.building" :building-id="buildingIdDisplayed"/>

		<!-- 对话框（产线图说明） -->
		<el-dialog v-model="dialogVisible.instruction" :close-on-click-modal="false" :close-on-press-escape="true" append-to-body center class="cus-dialog"
		           destroy-on-close draggable title="产线图说明" width="35%">
			<el-space alignment="normal" direction="vertical">
				<el-text>1. 拖拽：按住鼠标左键</el-text>
				<el-text>2. 缩放：Ctrl + 鼠标滚轮，或点击鼠标中键在常规和缩放模式之间切换</el-text>
				<el-text>3. 点击物品或建筑图片可查看详细信息</el-text>
				<el-text>4. <span style="color: #D57616">橙色<span style="font-size: x-large">○</span></span>表示物品，<span style="color: #409EFF">蓝色<span
						style="font-size: x-large">□</span></span>表示生产建筑，<span style="color: #9370DB">紫色<span style="font-size: x-large">○</span></span>表示副产物
				</el-text>
				<el-text>5. <span style="color: #F55656">&nbsp;溢出&nbsp;</span>字样表示物品的速率已超过顶级传送带或管道的运力</el-text>
			</el-space>
			<template #footer>
				<el-button icon="el-icon-CircleClose" type="warning" @click="dialogVisible.instruction = false">
					关闭
				</el-button>
			</template>
		</el-dialog>

		<!-- 超频设置 -->
		<overclock-slider ref="overclockSliderRef" @confirmOverclock="confirmOverclock"></overclock-slider>

	</div>
</template>

<script lang="ts" name="ProductionLine2" setup>
import {
	elTreeIterator,
	findInTreeArray,
	focusFormErrorElement,
	getImage,
	greatestCommonDivisor,
	isResourceCollectionRecipe,
	messageError,
	MinerNames,
	numberToPercent,
	ResourceCollectorNames,
	smallestCommonMultiple,
	treeArrayIterator,
	treeArrayToArray,
	treeIterator
} from '@/utils/commonUtil'
import {OVERCLOCK_PERCENTAGE_MARKS, POWER_SHARD_ID} from '@/utils/constantsUtil'
import Icon from '@/components/icon.vue'
import {ElForm, ElLink, ElMessageBox, ElSwitch, type FormInstance, type FormRules} from 'element-plus'
import BusinessCodeSelect from '@/components/business-code-select.vue'
import CusForm from '@/components/cus-form.vue'
import {computed, nextTick, reactive, ref, watch} from 'vue'
import {getAllItemApi} from '@/api/gameData/recipe.api'
import Fraction from 'fraction.js'
import {GProductLineService} from '@/service/GProductLineService'
import * as go from 'gojs'
import {GraphObject, InputEvent, Point, TreeLayout} from 'gojs'
import {cloneDeep, debounce, type DebouncedFunc, map, unset} from 'lodash-es'
import {calcMaxHeight} from '@/utils/layoutUtil'
import OverclockSlider from '@/views/gameTool/components/overclockSlider.vue'
import CusBuildingDialog from '@/components/cus-building-dialog.vue'
import CusItemDialog from '@/components/cus-item-dialog.vue'
import {isVisitor} from '@/utils/authUtil'
import CusTableTitle from '@/components/cus-table-title.vue'
import type Node from 'element-plus/es/components/tree/src/model/node'
import {generateProductionNameApi, saveUserProductionApi} from '@/api/gameTool/productionLine.api'
import {routePush} from '@/utils/routeUtil'
import {useRoute} from 'vue-router'
import {useUserStore} from '@/stores/modules/user.store'
import {useRouteParamsStore} from '@/stores/modules/routeParams.store'

/*
	============================================= 公共 - 开始 =============================================
 */

const route = useRoute()

watch(() => route.path, () => {
	for (const key in drawerVisible) {
		drawerVisible[<keyof typeof drawerVisible>key] = false
	}
})

const userStore = useUserStore()
const routeParamsStore = useRouteParamsStore()

const dialogVisible = reactive({
	item: false,
	building: false,
	instruction: false
})

const drawerVisible = reactive({
	chooseRecipe: false,
	statistics: false
})

/*
	============================================= 公共 - 结束 =============================================
 */

/*
	============================================= 搜索表单 - 开始 =============================================
 */

const formRef = ref<FormInstance>()

const params = reactive<{
	selectedItem?: GItem
	quantityPerMinute: number
	defaultMiner: number
	defaultExpandAll: boolean
	defaultOverclock: number
}>({
	selectedItem: undefined,
	quantityPerMinute: 10,
	defaultMiner: 0,
	defaultExpandAll: true,
	defaultOverclock: 1
})

const rules = reactive<FormRules>({
	selectedItem: [
		{required: true, message: '请选择物品', trigger: 'change'}
	],
	quantityPerMinute: [
		{required: true, message: '请输入产出速度', trigger: 'change'}
	],
	defaultMiner: [
		{required: true, message: '请选择默认采矿机', trigger: 'change'}
	],
	defaultExpandAll: [
		{required: true, message: '请选择是否展开子树', trigger: 'change'}
	],
	defaultOverclock: [
		{required: true, message: '请选择默认超频倍率', trigger: 'change'}
	]
})

const itemOptionList = ref<GItem[]>([])
getAllItemApi().then(response => {
	itemOptionList.value = response.data
})

/*
	============================================= 搜索表单 - 结束 =============================================
 */

/*
	============================================= 产线优化 - 开始 =============================================
 */

const optimizeParams = reactive<{
	considerMiner: boolean
}>({
	considerMiner: false
})

const optimizedQuantityPerMinute = ref<number>()

/**
 * 获取图表links中的所有建筑数量的分数（考虑超频）
 */
const getAllBuildingCountExactFraction = (): FractionInterface[] => {
	const result: FractionInterface[] = []
	for (const link of links.value) {
		// 判断是否需要优化资源采集建筑
		if (optimizeParams.considerMiner) {
			if (link.countExactFraction) {
				result.push(link.countExactFraction)
			}
		} else {
			const node = findInTreeArray(treeRef.value!.root.childNodes, node => node.id === link.from, 'childNodes')
			if (!node) {
				throw Error('无法匹配节点')
			}
			// node.childNodes.length > 0：有原料说明不是资源采集建筑
			if (link.countExactFraction && node.childNodes.length > 0) {
				result.push(link.countExactFraction)
			}
		}
	}
	return result
}

/**
 * 判断产线是否需要优化
 */
const judgeOptimization = () => {
	if (!treeRef.value) {
		return
	}
	const allBuildingCountExactFraction = getAllBuildingCountExactFraction()
	if (allBuildingCountExactFraction.length > 0) {
		// 筛选出所有分母
		const denominatorArray = map(allBuildingCountExactFraction, 'denominator')
		// 计算分母的最小公倍数
		const smallestCommonMultipleVal = smallestCommonMultiple(denominatorArray).result
		// 如果最小公倍数大于1（不等于1），则每个表示建筑数量的分数都乘以这个公倍数，得到的新的建筑数量就都是整数了，再计算是否可以缩小
		// 如果最小公倍数等于1，说明此时的建筑数量已经都为整数，直接计算是否可以缩小
		let allBuildingCountExactDecimalNew = []
		if (smallestCommonMultipleVal > 1) {
			// 计算乘以最小公倍数后建筑的数量
			for (const buildingCountExactFraction of allBuildingCountExactFraction) {
				allBuildingCountExactDecimalNew.push(buildingCountExactFraction.numerator.mul(smallestCommonMultipleVal).div(buildingCountExactFraction.denominator))
			}
		} else {
			allBuildingCountExactDecimalNew = map(allBuildingCountExactFraction, 'numerator')
		}
		// 求最大公约数，以便判断是否可以缩小
		const greatestCommonDivisorVal = greatestCommonDivisor(allBuildingCountExactDecimalNew)
		// 获取当前的产出速度
		const quantityPerMinute = treeRef.value!.root.childNodes[0].data.quantityPerMinute
		// 新的产出速度
		optimizedQuantityPerMinute.value = quantityPerMinute.mul(smallestCommonMultipleVal).div(greatestCommonDivisorVal)
		// 显示优化结果
		if (smallestCommonMultipleVal > greatestCommonDivisorVal) {
			needOptimization.value = true
			optimizationTip.value = '生产规模扩大为原来的 ' + smallestCommonMultipleVal + (greatestCommonDivisorVal === 1 ? '' : '/' + greatestCommonDivisorVal) + ' 倍，新产出速度为：' + optimizedQuantityPerMinute.value + '/min'
		} else if (smallestCommonMultipleVal < greatestCommonDivisorVal) {
			needOptimization.value = true
			optimizationTip.value = '生产规模缩小为原来的 ' + smallestCommonMultipleVal + '/' + greatestCommonDivisorVal + '，新产出速度为：' + optimizedQuantityPerMinute.value + '/min'
		} else {
			needOptimization.value = false
			optimizationTip.value = '无'
		}
	} else {
		needOptimization.value = false
		optimizationTip.value = '无'
	}
}

/**
 * 应用优化
 */
const optimize = () => {
	params.quantityPerMinute = optimizedQuantityPerMinute.value!
	calculate()
}

/*
	============================================= 产线优化 - 结束 =============================================
 */

/*
	============================================= 产线图 - 开始 =============================================
 */

const needOptimization = ref(false)
const optimizationTip = ref<string>()
const lineVisible = ref(false)

/**
 * 记忆节点选择的配方
 * key：代表当前节点路径的字符串，从路径数组转化而来
 * value：recipeId
 */
const memorizedRecipeMap = new Map<string, number>()

/**
 * 记忆不展开的节点
 * key：代表当前节点路径的字符串，从路径数组转化而来
 */
const memorizedNotLoadedNodeSet = new Set<string>()

/**
 * 记忆节点的超频倍率
 * key：代表当前节点路径的字符串，从路径数组转化而来
 * value：超频倍率
 */
const memorizedOverclockMap = new Map<string, number>()

const productLineService = new GProductLineService()

const treeData = ref<GoJsDatum2[]>([])
const links = ref<GoJsLinkDatum2[]>([])

const $ = go.GraphObject.make
const myDiagramLoading = ref(false)
let diagram: go.Diagram | undefined
const diagramDirection = ref<'horizontal' | 'vertical'>('horizontal')
const diagramAngle = computed<number>(() => diagramDirection.value === 'horizontal' ? 0 : 90)

/**
 * 连线模板
 */
const diagramLinkTemplate = computed(() => {
	return new go.Link({
		routing: go.Link.AvoidsNodes,
		fromSpot: go.Spot.Default,
		toSpot: go.Spot.Default,
		fromEndSegmentLength: 40,
		corner: 10
	})
			// 连线
			.add(new go.Shape({stroke: '#D57616'}).bind('visible', 'hideLink', val => !val))
			// 箭头
			// .add(new go.Shape({toArrow: 'Standard', stroke: '#D57616', fill: '#D57616'}))
			// 图片和文字
			.add(new go.Panel('Vertical', {
						segmentIndex: 0,
						segmentFraction: 0,
						segmentOffset: new Point(diagramDirection.value === 'horizontal' ? 25 : -60, diagramDirection.value === 'horizontal' ? 12 : -70)
					})
							.bind('visible', 'showBuilding')
							// 建筑图片
							.add(new go.Panel('Auto', {margin: 5})
									.add(new go.Shape('RoundedRectangle', {fill: '#141414FF', stroke: '#409eff'}))
									.add(new go.Picture({width: 50, height: 50, click: handleClickBuildingImage, cursor: 'pointer'}).bind('source', 'image', getImage)))
							// 建筑名称
							.add(new go.TextBlock({stroke: '#409eff'}).bind('text', 'buildingName'))
							// 建筑数量
							.add(new go.TextBlock({stroke: '#409eff'}).bind('visible', 'count', val => val > 0).bind('text', 'buildingCountStr'))
							// 超频按钮
							.add($('Button', {
												margin: 2,
												'ButtonBorder.fill': '#909399',
												'ButtonBorder.stroke': '#909399',
												'_buttonFillOver': '#b8bbc1',
												'_buttonStrokeOver': '#b8bbc1',
												'_buttonFillPressed': '#b8bbc1',
												'_buttonStrokePressed': '#b8bbc1',
												click: toOverclock
											},
											$(go.TextBlock).bind('text', 'overclock', val => val === 1 ? '不超频' : val < 1 ? `降频:${numberToPercent(val)}` : `超频:${numberToPercent(val)}`))
											.bind('visible', 'showOverclockable')
											.bind('ButtonBorder.fill', 'overclock', val => val > 1 ? 'rgba(255,255,0,0.8)' : val < 1 ? 'rgba(64,158,255,0.8)' : 'rgba(184,187,193,0.8)')
											.bind('ButtonBorder.stroke', 'overclock', val => val > 1 ? 'rgba(255,255,0,0.8)' : val < 1 ? 'rgba(64,158,255,0.8)' : 'rgba(184,187,193,0.8)')
											.bind('_buttonFillOver', 'overclock', val => val > 1 ? 'rgba(255,255,0,1)' : val < 1 ? 'rgba(64,158,255,1)' : 'rgba(184,187,193,1)')
											.bind('_buttonStrokeOver', 'overclock', val => val > 1 ? 'rgba(255,255,0,1)' : val < 1 ? 'rgba(64,158,255,1)' : 'rgba(184,187,193,1)')
											.bind('_buttonFillPressed', 'overclock', val => val > 1 ? 'rgba(255,255,0,1)' : val < 1 ? 'rgba(64,158,255,1)' : 'rgba(184,187,193,1)')
											.bind('_buttonStrokePressed', 'overclock', val => val > 1 ? 'rgba(255,255,0,1)' : val < 1 ? 'rgba(64,158,255,1)' : 'rgba(184,187,193,1)')
							)
							// 副产物
							.add(new go.Panel('Vertical', {
										itemTemplate:
												$(go.Panel, 'Vertical',
														$(go.Panel, 'Auto', {margin: 5},
																$(go.Shape, 'Ellipse', {fill: '#141414FF', stroke: '#9370DB'}),
																$(go.Picture, {width: 35, height: 35, click: handleClickItemImage, cursor: 'pointer'}).bind('source', 'image', getImage)
														),
														$(go.Panel, 'Vertical',
																$(go.TextBlock, {stroke: '#9370DB'}).bind('text', 'itemName'),
																$(go.TextBlock, {stroke: '#9370DB'}).bind('text', 'quantityPerMinuteStr')
														)
												)
									}).bind('itemArray', 'byProducts')
							)
			)
})

/**
 * 产线图自适应高度
 */
const diagramMaxHeight = ref(800)
const calcDiagramMaxHeight: DebouncedFunc<() => typeof calcDiagramMaxHeight> = debounce(() => {
	diagramMaxHeight.value = calcMaxHeight('#productionLine-form-01', '#productionLine-form-02', '#productionLine-table-title')
	return calcDiagramMaxHeight
}, 100, {
	leading: true,
	trailing: false,
	maxWait: 100
})

/**
 * 初始化产线图
 */
const initLine = () => {
	lineVisible.value = true
	nextTick(() => {
		myDiagramLoading.value = true
		// 创建图表
		diagram = new go.Diagram('productionLineDiagram')
		// 节点模版
		diagram.nodeTemplate = new go.Node('Vertical')
				.add(new go.Panel('Auto', {margin: 5})
						.bind('visible', 'hideNode', val => !val)
						.add(new go.Shape('Ellipse', {fill: '#141414FF', stroke: '#D57616'}))
						.add(new go.Picture({width: 75, height: 75, click: handleClickItemImage, cursor: 'pointer'}).bind('source', 'image', getImage))
				)
				.add(new go.Panel('Vertical')
						.bind('visible', 'hideNode', val => !val)
						// 物品名称
						.add(new go.TextBlock({stroke: '#D57616'}).bind('text', 'itemName'))
						// 分钟数量
						.add(new go.TextBlock({stroke: '#D57616'}).bind('visible', 'quantityPerMinute', val => val > 0).bind('text', 'quantityPerMinuteStr'))
						// 更换配方按钮
						.add($('Button',
								{
									margin: 2,
									'ButtonBorder.fill': '#909399',
									'ButtonBorder.stroke': '#909399',
									'_buttonFillOver': '#b8bbc1',
									'_buttonStrokeOver': '#b8bbc1',
									'_buttonFillPressed': '#b8bbc1',
									'_buttonStrokePressed': '#b8bbc1',
									click: toChooseRecipeFromGoJs
								},
								$(go.TextBlock, {text: '更换配方'})).bind('visible', 'recipeButtonVisible'))
						// .add($('TreeExpanderButton'))
						// 展开收起按钮
						.add($('Button', {
							'ButtonBorder.fill': '#909399',
							'ButtonBorder.stroke': '#909399',
							'_buttonFillOver': '#b8bbc1',
							'_buttonStrokeOver': '#b8bbc1',
							'_buttonFillPressed': '#b8bbc1',
							'_buttonStrokePressed': '#b8bbc1',
							click: handleDiagramNodeExpand
						}, $(go.TextBlock).bind('text', 'expandButtonText'))
								.bind('visible', 'expandButtonVisible'))
				)
		// 连线模版
		diagram.linkTemplate = diagramLinkTemplate.value
		// 布局
		const treeLayout = new go.TreeLayout()
		treeLayout.nodeSpacing = 125
		treeLayout.layerSpacing = 125
		treeLayout.angle = diagramAngle.value
		diagram.layout = treeLayout
		// 禁止选中和拖拽
		diagram.allowSelect = false
	})
}

/**
 * 计算产线
 */
const calculate = () => {
	overclockSliderRef.value?.close()
	formRef.value!.validate(valid => {
		if (valid) {
			// 按需记忆
			if (treeRef.value && treeRef.value.root.childNodes[0]) {
				// 如果没有更换产物，需要记忆当前选择的配方和超频倍率
				if (treeRef.value.root.childNodes[0].data.itemId === params.selectedItem!.id) {
					buildMemorizedRecipeMap()
					buildMemorizedOverclockMap()
				}
				// 如果更换了产物，需要清空记忆
				else {
					memorizedNotLoadedNodeSet.clear()
					memorizedRecipeMap.clear()
					memorizedOverclockMap.clear()
				}
			}
			// 清空原数据
			treeData.value = []
			links.value = []
			// 初始化产线图
			initLine()
			// 添加根节点
			const quantityPerMinuteFraction = new Fraction(params.quantityPerMinute)
			const root: GoJsDatum2 = {
				itemId: params.selectedItem.id,
				itemName: params.selectedItem.itemName,
				quantityPerMinute: params.quantityPerMinute,
				quantityPerMinuteExactFraction: {
					numerator: quantityPerMinuteFraction.n,
					denominator: quantityPerMinuteFraction.d
				},
				image: params.selectedItem.image,
				fluid: params.selectedItem.fluid,
				// TODO
				key: 0,
				loaded: false,
				hideNode: false,
				recipeButtonVisible: false,
				expanded: false,
				expandButtonVisible: false,
				expandButtonText: '',
				children: []
			}
			treeData.value.push(root)
			// 查询下级配方
			const loadOneStepRequest: LoadOneStepRequest = {
				itemId: params.selectedItem.id,
				quantityPerMinuteExactFraction: {
					numerator: quantityPerMinuteFraction.n,
					denominator: quantityPerMinuteFraction.d
				}
			}
			productLineService.loadOneStep(loadOneStepRequest).then(loadOneStepVoList => {
				addChildren(root, loadOneStepVoList)
			})
		} else {
			focusFormErrorElement()
		}
	})
}

const addChildren = (parent: GoJsDatum2, loadOneStepVoList: LoadOneStepVo[]) => {
	if (!parent.children) {
		parent.children = []
	}
	if (!loadOneStepVoList || loadOneStepVoList.length === 0) {
		console.info(`没有生产 ${parent.itemName} 的配方`)
		parent.loaded = true
		checkLineFullyLoaded()
		return
	}
	parent.recipeList = loadOneStepVoList
	// TODO 选择默认配方
	const loadOneStepVo = loadOneStepVoList[0]
	if (!loadOneStepVo.materialList || loadOneStepVo.materialList.length === 0) {
		console.info(`生产 ${parent.itemName} 无需原料`)
		parent.loaded = true
		checkLineFullyLoaded()
		return
	}
	for (const materialVo of loadOneStepVo.materialList) {
		const child = {
			itemId: materialVo.itemId,
			itemName: materialVo.itemName,
			quantityPerMinute: materialVo.quantityPerMinute,
			quantityPerMinuteExactFraction: materialVo.quantityPerMinuteExactFraction,
			image: materialVo.image,
			fluid: materialVo.fluid,
			// TODO
			key: 0,
			loaded: false,
			hideNode: false,
			recipeButtonVisible: false,
			expanded: false,
			expandButtonVisible: false,
			expandButtonText: '',
			children: []
		}
		parent.children.push(child)
		// 继续查询下级配方
		const loadOneStepRequest: LoadOneStepRequest = {
			itemId: child.itemId,
			quantityPerMinuteExactFraction: child.quantityPerMinuteExactFraction
		}
		productLineService.loadOneStep(loadOneStepRequest).then(loadOneStepVoList => {
			addChildren(child, loadOneStepVoList)
		})
	}
	parent.loaded = true
}

/**
 * 检查产线图是否加载完毕
 */
const checkLineFullyLoaded = () => {
	if (treeData.value.length === 0) {
		return
	}
	const find = findInTreeArray(treeData.value, datum => datum.loaded === false)
	if (find) {
		return
	}
	console.log('产线图加载完毕', treeData.value)
	buildDiagram()
	judgeOptimization()
}

/**
 * 构建产线图数据
 */
const buildDiagram = () => {
	if (!treeRef.value) {
		return
	}
	// 清空原数据
	links.value = []
	treeData.value = [treeRef.value.root.childNodes[0].data]
	treeArrayIterator(treeData.value, (datum: GoJsDatum) => {
		datum.treeNodeId = datum.$treeNodeId
		const node = findInTreeArray(treeRef.value!.root.childNodes, node => node.id === datum.treeNodeId, 'childNodes')
		if (!node) {
			throw Error('无法匹配节点')
		}
		datum.loaded = node.loaded
		datum.children = []
		for (const childNode of node.childNodes) {
			datum.children.push(<GoJsDatum>childNode.data)
		}
	})
	treeData.value = cloneDeep(treeData.value)
	// 数据处理
	treeArrayIterator(treeData.value, datum => {
		// 处理部分属性
		datum.key = datum.treeNodeId!
		// 分钟数量转化为更精确的形式
		if (datum.quantityPerMinute) {
			const fraction = new Fraction(datum.quantityPerMinuteExactFraction.numerator, datum.quantityPerMinuteExactFraction.denominator)
			datum.quantityPerMinute = Math.round(fraction.valueOf() * 100) / 100
			datum.quantityPerMinuteStr = datum.quantityPerMinute + '/min'
		}
		datum.expanded = true
		// 有多个制作配方则显示更换配方按钮
		datum.recipeButtonVisible = datum.loaded && (datum.recipeList ? datum.recipeList.length > 1 : false)
		// 没有制作配方则不显示展开和收起按钮
		datum.expandButtonVisible = !datum.loaded || (datum.recipeList ? datum.recipeList.length > 0 : false)
		datum.expandButtonText = datum.loaded ? (datum.recipeList && datum.recipeList.length > 0 ? '收起' : undefined) : '展开'
		// 资源采集配方没有原料，只有生产建筑，手动添加下级节点用于显示资源采集建筑，如采矿机、抽水站、油井等
		const recipeDisplayed = datum.recipeList?.find(recipe => recipe.recipeId === datum.recipeId)
		const isResourceCollectionRecipeFlag = !!(recipeDisplayed && isResourceCollectionRecipe(recipeDisplayed))
		// 找出树中最大的treeNodeId
		let maxTreeNodeId = 0
		treeArrayIterator(treeData.value, datum => {
			if (datum.treeNodeId && datum.treeNodeId > maxTreeNodeId) {
				maxTreeNodeId = datum.treeNodeId
			}
		})
		if (isResourceCollectionRecipeFlag) {
			maxTreeNodeId++
			datum.children.push({
				itemId: undefined,
				itemName: undefined,
				quantityPerMinute: undefined,
				quantityPerMinuteExactFraction: undefined,
				image: undefined,
				fluid: undefined,
				recipeId: undefined,
				recipeList: undefined,
				buildingVo: datum.buildingVo,
				handmadeBuildingVoList: [],
				byProductList: [],
				$treeNodeId: undefined,
				treeNodeId: maxTreeNodeId,
				key: maxTreeNodeId,
				quantityPerMinuteStr: undefined,
				loaded: true,
				hideNode: true,
				recipeButtonVisible: false,
				expanded: true,
				expandButtonVisible: undefined,
				expandButtonText: undefined,
				children: []
			})
		}
		// 构建下级links（原料）
		if (datum.children && datum.children.length > 0) {
			for (const child of datum.children) {
				const buildingInfo = getBuildingInfo(datum)
				// 获取超频倍率，优先级：超频记忆 > 默认超频倍率 > 不超频
				const node = findInTreeArray(treeRef.value!.root.childNodes, node => node.id === datum.treeNodeId, 'childNodes')
				if (!node) {
					throw Error('无法匹配节点')
				}
				const overclockInMemory = memorizedOverclockMap.get(findPathToNode(node).toString())
				const overclock = overclockInMemory || params.defaultOverclock || 1
				const fraction = buildingInfo.countExactFraction && new Fraction(buildingInfo.countExactFraction.numerator, buildingInfo.countExactFraction.denominator).div(overclock)
				const countExactFraction = buildingInfo.countExactFraction && {
					numerator: fraction.n,
					denominator: fraction.d
				}
				const countExact = fraction && Math.round(fraction.valueOf() * 100) / 100
				const link: GoJsLinkDatum = {
					from: datum.treeNodeId!,
					to: child.treeNodeId!,
					image: buildingInfo.image,
					buildingId: buildingInfo.buildingId,
					buildingName: buildingInfo.buildingName,
					hideLink: isResourceCollectionRecipeFlag,
					showBuilding: datum.children.indexOf(child) === datum.children.length - 1,
					overclockable: buildingInfo.overclockable,
					showOverclockable: buildingInfo?.overclockable === 1 && buildingInfo.count > 0,
					overclock: overclock,
					count: buildingInfo.count,
					countExact: countExact,
					countExactFraction: countExactFraction,
					countExactFractionOriginal: buildingInfo.countExactFraction,
					buildingCountStr: buildingInfo.count + (buildingInfo.count !== buildingInfo.countExact ? '(' + countExact + ')' : '')
				}
				// 如果有副产物，则显示在生产建筑下方
				if (datum.byProductList && datum.byProductList.length > 0) {
					link.byProducts = datum.byProductList
					for (const byProduct of link.byProducts) {
						byProduct.quantityPerMinuteStr = byProduct.quantityPerMinute ? (byProduct.quantityPerMinute + '/min') : undefined
					}
				}
				links.value.push(link)
			}
		}
	})
	// 绑定数据
	diagram.model = new go.GraphLinksModel(treeArrayToArray(treeData.value), links.value)
	// 自动缩放
	// diagram.autoScale = go.Diagram.UniformToFill
	buildDiagramFinish()
}

const getBuildingInfo = (data: GoJsDatum) => {
	if (data.buildingVo) {
		return data.buildingVo
	}
	if (data.handmadeBuildingVoList && data.handmadeBuildingVoList.length > 0) {
		return data.handmadeBuildingVoList[0]
	}
	return undefined
}

const buildDiagramFinish = debounce(() => {
	// diagram.autoScale = go.Diagram.None
	diagram.focus()
	calcDiagramMaxHeight()
	myDiagramLoading.value = false
}, 500, {
	leading: false,
	trailing: true,
	maxWait: 500
})

/**
 * 重置
 */
const resetLine = () => {
	formRef.value!.resetFields()
	memorizedRecipeMap.clear()
	memorizedNotLoadedNodeSet.clear()
	memorizedOverclockMap.clear()
	treeRef.value?.remove(treeRef.value.root.childNodes[0])
	needOptimization.value = false
	optimizationTip.value = undefined
	optimizedQuantityPerMinute.value = undefined
	recipeTableData.value = []
	basicMaterialList.value = []
	byProductList.value = []
	productionLineBuildingList.value = []
	totalPower.value = 0
	othersList.value = []
	operatingNode.value = undefined
	selectedRecipeId.value = undefined
	for (const key in resolveMap) {
		delete resolveMap[key]
	}
	for (const key in stopLoadingMap) {
		delete stopLoadingMap[key]
	}
	if (diagram) {
		diagram.reset()
		diagram = undefined
	}
	lineVisible.value = false
}

const buildMemorizedRecipeMap = () => {
	treeIterator(treeRef.value!.root.childNodes[0], node => {
		// 判断是否有替代配方，有则记忆选择的配方
		if (node.data && node.data.recipeList && node.data.recipeList.length > 1) {
			memorizedRecipeMap.set(findPathToNode(node).toString(), node.data.recipeId)
		}
	}, 'childNodes')
}

const buildMemorizedOverclockMap = () => {
	links.value.filter(link => link.overclockable === 1).forEach(link => {
		const node = findInTreeArray(treeRef.value!.root.childNodes, node => node.id === link.from, 'childNodes')
		memorizedOverclockMap.set(findPathToNode(node).toString(), link.overclock)
	})
}

const handleDiagramNodeExpand = (e: InputEvent, thisObj: GraphObject) => {
	const node: Node = findInTreeArray(treeRef.value!.root.childNodes, node => node.id === thisObj.part?.data.treeNodeId, 'childNodes')
	const data = node.data
	const loaded = data.loaded
	const pathToNode = findPathToNode(node).toString()
	if (loaded) {
		data.expandButtonText = '展开'
		// 记录节点收起前选择的配方
		if (data.recipeId) {
			memorizedRecipeMap.set(pathToNode, data.recipeId)
		}
		// 节点收起后会重新生成产线图，因此需要记忆超频倍率
		buildMemorizedOverclockMap()
		// 清除节点数据
		data.recipeId = undefined
		data.recipeList = []
		data.byProductList = []
		data.buildingVo = undefined
		data.handmadeBuildingVoList = []
		node.loaded = false
		node.expanded = false
		// 删除下级节点的resolve
		if (node.childNodes.length > 0) {
			treeArrayIterator(node.childNodes, node => {
				delete resolveMap[node.id]
				delete stopLoadingMap[node.id]
			}, 'childNodes')
		}
		// 删除下级节点
		while (node.childNodes.length > 0) {
			treeRef.value!.remove(node.childNodes[0])
		}
		// 记录不展开的节点
		memorizedNotLoadedNodeSet.add(pathToNode)
		// 后续
		afterTreeLoadedNoDelay()
	} else {
		data.expandButtonText = '收起'
		// 删除不展开的节点
		memorizedNotLoadedNodeSet.delete(pathToNode)
		// 加载下级节点
		node.expand()
	}
}

const handleClickItemImage = (e: InputEvent, thisObj: GraphObject) => {
	let obj: any = thisObj
	// 防止无限循环
	let step = 0
	do {
		obj = obj.panel
		step++
	} while (!obj.data && step < 100)
	const data = obj.data
	if (data.itemId) {
		showItemDetail(data.itemId)
	} else {
		throw Error('无法获取物品数据')
	}
}

const handleClickBuildingImage = (e: InputEvent, thisObj: GraphObject) => {
	let obj: any = thisObj
	// 防止无限循环
	let step = 0
	do {
		obj = obj.panel
		step++
	} while (!obj.data && step < 100)
	const data = obj.data
	if (data.buildingId) {
		showBuildingDetail(data.buildingId)
	} else {
		throw Error('无法获取建筑数据')
	}
}

const itemIdDisplayed = ref<number>()
const showItemDetail = (itemId: number) => {
	itemIdDisplayed.value = itemId
	dialogVisible.item = true
}

const buildingIdDisplayed = ref<number>()
const showBuildingDetail = (buildingId: number) => {
	buildingIdDisplayed.value = buildingId
	dialogVisible.building = true
}

const toggleDirection = () => {
	if (!diagram) {
		return
	}
	if (diagramDirection.value === 'horizontal') {
		diagramDirection.value = 'vertical'
	} else {
		diagramDirection.value = 'horizontal'
	}
	// 图表方向
	(<TreeLayout>diagram.layout).angle = diagramAngle.value
	// 连线模版
	diagram.linkTemplate = diagramLinkTemplate.value
	diagram.layoutDiagram()
}

/**
 * 保存为图片
 */
const saveAsImage = () => {
	const imgData = diagram.makeImageData({
		returnType: 'image/png',
		scale: 1,
		maxSize: new go.Size(Infinity, Infinity)
	})
	// 创建一个链接元素来下载图像
	const link = document.createElement('a')
	link.href = <string>imgData
	link.download = `${params.selectedItem.itemName}（${params.quantityPerMinute}_per_min）.png`
	document.body.appendChild(link)
	link.click()
	document.body.removeChild(link)
}

/**
 * 产线图说明
 */
const showInstructions = () => {
	dialogVisible.instruction = true
}

/**
 * 获取在同级节点中的位置
 */
const getIndex = (node: Node) => {
	if (!node.parent) {
		return 0
	} else {
		for (let i = 0; i < node.parent.childNodes.length; i++) {
			if (node.parent.childNodes[i] === node) {
				return i
			}
		}
	}
	return false
}

/**
 * 返回表示某个节点的访问路径的数组
 */
const findPathToNode = (target: Node, node?: Node, arr?: number[]) => {
	if (!treeRef.value) {
		throw Error('树还未初始化')
	}
	if (!target) {
		throw Error('目标节点为空')
	}
	if (!node) {
		node = treeRef.value.root
	}
	if (!arr) {
		arr = []
	}
	if (node === target) {
		const index = getIndex(node)
		if (index === false) {
			throw Error('无法匹配节点')
		}
		arr.unshift(index)
		return node === treeRef.value.root ? arr : true
	}
	// 继续找下级节点
	else {
		let result = false
		for (const child of node.childNodes) {
			if (findPathToNode(target, child, arr)) {
				result = true
			}
		}
		if (result) {
			const index = getIndex(node)
			if (index === false) {
				throw Error('无法匹配节点')
			}
			arr.unshift(index)
		}
		return result && node === treeRef.value.root ? arr : result
	}
}

/*
	============================================= 产线图 - 结束 =============================================
 */

/*
	============================================= 产线收藏 - 开始 =============================================
 */

const favorButtonLoading = ref(false)

/**
 * 收藏产线
 */
const favor = () => {
	if (!treeRef.value?.root.childNodes[0].data) {
		messageError('请先计算产线')
		return
	}
	favorButtonLoading.value = true
	// 构建统计数据
	buildStatistics()
	// data字段的数据
	const data: Omit<ProductionLineDataForDiagram, 'productionLineId'> = {
		treeData: cloneDeep(treeData.value),
		links: cloneDeep(links.value),
		statistics: {
			basicMaterialList: basicMaterialList.value,
			byProductList: byProductList.value,
			productionLineBuildingList: productionLineBuildingList.value,
			totalPower: totalPower.value,
			othersList: othersList.value
		}
	}
	// 删除treeData中的多余属性：buildingVo、byProductList、expandButtonText、expandButtonVisible、expanded、fluid、handmadeBuildingVoList、
	// loaded、quantityPerMinute、quantityPerMinuteExactFraction、recipeButtonVisible、recipeId、recipeList、treeNodeId
	treeArrayIterator(data.treeData, data => {
		unset(data, 'buildingVo')
		unset(data, 'byProductList')
		unset(data, 'expandButtonText')
		unset(data, 'expandButtonVisible')
		unset(data, 'expanded')
		unset(data, 'fluid')
		unset(data, 'handmadeBuildingVoList')
		unset(data, 'loaded')
		unset(data, 'quantityPerMinuteExactFraction')
		unset(data, 'recipeButtonVisible')
		unset(data, 'recipeId')
		unset(data, 'recipeList')
		unset(data, 'treeNodeId')
	})
	// 删除links中的多余属性：__gohashid
	// 删除byProducts中的多余属性：__gohashid、quantityPerMinute、quantityPerMinuteExactFraction、quantityPerProduction
	for (const link of data.links) {
		unset(link, '__gohashid')
		const byProducts = link.byProducts
		if (byProducts && byProducts.length > 0) {
			for (const byProduct of byProducts) {
				unset(byProduct, '__gohashid')
				unset(byProduct, 'quantityPerMinute')
				unset(byProduct, 'quantityPerMinuteExactFraction')
				unset(byProduct, 'quantityPerProduction')
			}
		}
	}
	// 自动生成不重复的产线名称
	const req: GenerateProductionNameRequest = {
		type: 'DIAGRAM',
		userId: userStore.userId!,
		productionName: `${treeData.value[0].itemName}（${treeData.value[0].quantityPerMinute}/min）` + (treeData.value[0].handmadeBuildingVoList && treeData.value[0].handmadeBuildingVoList.length > 0 ? '（手搓）' : '')
	}
	generateProductionNameApi(req).then(response => {
		ElMessageBox.prompt('产线名称：', '收藏产线', {
			confirmButtonText: '确定',
			cancelButtonText: '取消',
			draggable: true,
			inputValue: response.data,
			inputPlaceholder: '请输入产线名称'
		}).then(({value}) => {
			if (value) {
				value = value.trim()
			}
			const request: SaveUserProductionRequest = {
				type: 2,
				userId: userStore.userId!,
				productionName: value,
				itemId: treeData.value[0].itemId!,
				quantityPerMinute: treeData.value[0].quantityPerMinute!,
				data: JSON.stringify(data)
			}
			doSaveUserProduction(request)
		}).catch(() => {
		})
	}).finally(() => {
		favorButtonLoading.value = false
	})
}

const doSaveUserProduction = (request: SaveUserProductionRequest) => {
	saveUserProductionApi(request).then(response => {
		if (response.data) {
			ElMessageBox.prompt('产线名称已存在，请更换其他名称：', '收藏产线', {
				type: 'error',
				confirmButtonText: '确定',
				cancelButtonText: '取消',
				draggable: true,
				inputValue: request.productionName,
				inputPlaceholder: '请输入产线名称'
			}).then(({value}) => {
				if (value) {
					value = value.trim()
				}
				request.productionName = value
				doSaveUserProduction(request)
			}).catch(() => {
			})
		} else {
			ElMessageBox.confirm(`产线已收藏，请在【游戏工具 - 我的产线】中查看`, '成功', {
				confirmButtonText: '知道了',
				cancelButtonText: '去查看',
				type: 'success',
			}).then(() => {
			}).catch(() => {
				routeParamsStore.setParams({handleSearch: 1}, 'ProductionLineCollection')
				routePush({name: 'ProductionLineCollection'})
			})
		}
	}).catch(error => {
		ElMessageBox.prompt(error, '收藏产线', {
			type: 'error',
			confirmButtonText: '确定',
			cancelButtonText: '取消',
			draggable: true,
			inputValue: request.productionName,
			inputPlaceholder: '请输入产线名称'
		}).then(({value}) => {
			if (value) {
				value = value.trim()
			}
			request.productionName = value
			doSaveUserProduction(request)
		}).catch(() => {
		})
	})
}

/*
	============================================= 产线收藏 - 结束 =============================================
 */

/*
	============================================= 超频 - 开始 =============================================
 */

const overclockGraphObject = ref<GraphObject>(undefined)
const overclockTreeNode = ref<GoJsDatum2>(undefined)
const overclockSliderRef = ref()

/**
 * 超频
 */
const toOverclock = (e: InputEvent, thisObj: GraphObject) => {
	overclockGraphObject.value = thisObj
	// TODO 如何找到对应的节点
	overclockTreeNode.value = findInTreeArray(treeData.value, node => node.key === thisObj.part?.data.from, 'childNodes')
	const overclockData: OverclockData2 = {
		// 生产建筑
		building: thisObj.part?.data,
		// 产物
		item: overclockTreeNode.value
	}
	const {x, y} = <PointerEvent>e.event
	overclockSliderRef.value?.open(overclockData, {x, y})
}

const confirmOverclock = (overclock: number) => {
	overclockGraphObject.value!.part!.data.overclock = overclock
	// 计算新的建筑数量（分数）
	const fraction = new Fraction(overclockGraphObject.value!.part!.data.countExactFractionOriginal.numerator, overclockGraphObject.value!.part!.data.countExactFractionOriginal.denominator).div(overclock)
	const countExactFraction = {
		numerator: fraction.n,
		denominator: fraction.d
	}
	// 计算新的建筑数量（小数，保留两位小数）
	const countExact = Math.round(fraction.n.div(fraction.d) * 100) / 100
	// 计算新的建筑数量（进一法取整）
	const count = Math.ceil(countExact)
	// 计算新的建筑数量字符串
	let buildingCountStr = String(count)
	if (count !== countExact) {
		buildingCountStr += '(' + countExact + ')'
	}
	// 更新link数据（每条link都有一个建筑数据，每一个都要更新）
	links.value.filter(link => link.from === overclockGraphObject.value!.part!.data.from).forEach(link => {
		link.countExactFraction = countExactFraction
		link.countExact = countExact
		link.count = count
		link.buildingCountStr = buildingCountStr
	})
	// 保存当前视图位置和缩放级别
	const currentPosition = diagram.position.copy()
	const currentScale = diagram.scale
	// 绑定新数据
	diagram.model = new go.GraphLinksModel(treeArrayToArray(treeData.value), links.value)
	buildDiagramFinish()
	// 恢复视图位置和缩放级别
	diagram.position = currentPosition
	diagram.scale = currentScale
	diagram.requestUpdate()
	// 后续处理
	judgeOptimization()
}

/*
	============================================= 超频 - 结束 =============================================
 */

/*
	============================================= 更换配方 - 开始 =============================================
 */

const recipeTableData = ref<RecipeVo[]>([])

const recipeReplaceType = ref<'all' | 'current'>('current')

const operatingNode = ref<Node>()

const selectedRecipeId = ref<number>()

/**
 * 更换配方
 */
const toChooseRecipeFromGoJs = (e: InputEvent, thisObj: GraphObject) => {
	const data = thisObj.part?.data
	const find = findInTreeArray(treeRef.value?.root.childNodes!, (node: Node) => {
		return node.id === data.treeNodeId
	}, 'childNodes')
	if (!find) {
		throw Error('无法匹配节点')
	}
	operatingNode.value = find
	selectedRecipeId.value = data.recipeId
	recipeTableData.value = data.recipeList
	drawerVisible.chooseRecipe = true
}

const selectedRecipeChanged = () => {
	const operatingNodeList = []
	// 更换所有同类配方
	if (recipeReplaceType.value === 'all') {
		elTreeIterator(treeRef.value!, node => {
			if (node.data.itemId === operatingNode.value?.data.itemId) {
				operatingNodeList.push(node)
			}
		})
	}
	// 仅更换当前节点的配方
	else {
		operatingNodeList.push(operatingNode.value)
	}
	for (const node of operatingNodeList) {
		loadNode(node!, resolveMap[node!.id], stopLoadingMap[node!.id], selectedRecipeId.value)
		drawerVisible.chooseRecipe = false
		// 如果更换的是采矿机配方，则默认采矿机改为“使用当前配置”
		const selectedRecipe = recipeTableData.value.find(recipe => recipe.recipeId === selectedRecipeId.value)
		if (MinerNames.includes(selectedRecipe?.buildingVo?.buildingName!)) {
			params.defaultMiner = 0
		}
		// 更换配方后删除相关的记忆
		const changingPath = findPathToNode(node!).toString()
		// 删除配方记忆
		memorizedRecipeMap.delete(changingPath)
		for (const path of memorizedRecipeMap.keys()) {
			if (path.startsWith(changingPath) && path.replace(changingPath, '').startsWith(',')) {
				// 删除更换的节点的下级节点的记忆
				memorizedRecipeMap.delete(path)
			}
		}
		// 删除节点展开状态的记忆
		for (const path of memorizedNotLoadedNodeSet.values()) {
			if (path.startsWith(changingPath) && path.replace(changingPath, '').startsWith(',')) {
				// 删除更换的节点的下级节点的记忆
				memorizedNotLoadedNodeSet.delete(path)
			}
		}
		// 删除超频记忆
		memorizedOverclockMap.delete(changingPath)
		for (const path of memorizedOverclockMap.keys()) {
			if (path.startsWith(changingPath) && path.replace(changingPath, '').startsWith(',')) {
				// 删除更换的节点的下级节点的记忆
				memorizedOverclockMap.delete(path)
			}
		}
	}
}

/*
	============================================= 更换配方 - 结束 =============================================
 */

/*
	============================================= 数据统计 - 开始 =============================================
 */

const activeNames = ref(['material', 'byProduct', 'productionLineBuilding', 'power', 'others'])
const basicMaterialList = ref<MaterialVo[]>([])
const byProductList = ref<(ProductVo & { extra: number })[]>([])
const productionLineBuildingList = ref<ProductionLineBuildingVo[]>([])
const totalPower = ref(0)
const othersList = ref<StatisticsVo[]>([])

/**
 * 显示统计数据
 */
const showStatistics = () => {
	buildStatistics()
	drawerVisible.statistics = true
}

/**
 * 构建统计数据
 */
const buildStatistics = (node?: Node) => {
	// 计算原料
	buildBasicMaterials(node)
	// 原料按照数量倒序
	basicMaterialList.value.sort((a, b) => {
		return b.quantityPerMinute!.sub(a.quantityPerMinute!)
	})
	// 计算副产物、建筑、电力
	byProductList.value = []
	productionLineBuildingList.value = []
	totalPower.value = 0
	othersList.value = []
	elTreeIterator(treeRef.value!, statisticsHandler)
	// 检查副产物是否可以再次作为原料输入
	for (const byProduct of byProductList.value) {
		const find = basicMaterialList.value.find(material => material.itemId === byProduct.itemId)
		if (find) {
			byProduct.extra = byProduct.quantityPerMinute!.sub(find.quantityPerMinute!)
		}
	}
	// 副产物按照剩余量倒序
	byProductList.value.sort((a, b) => {
		return b.extra - a.extra
	})
	// 建筑按照数量倒序
	productionLineBuildingList.value.sort((a, b) => {
		return b.count!.sub(a.count!)
	})
	// 其他
	for (const link of links.value) {
		// 能量碎片
		const overclock = link.overclock
		if (link.overclockable === 1 && overclock > 1) {
			const powerShardCount = (overclock - 1) * 100 % 50 === 0 ? (overclock - 1) * 100 / 50 : Math.floor((overclock - 1) * 100 / 50) + 1
			const find = othersList.value.find(statisticsVo => statisticsVo.itemId === POWER_SHARD_ID)
			if (find) {
				find.value = <number>find.value + powerShardCount
			} else {
				othersList.value.push({
					label: '能量碎片（超频）',
					value: powerShardCount,
					suffix: ' 个',
					image: '物品/能量碎片.png',
					itemId: POWER_SHARD_ID
				})
			}
		}
	}
}

/**
 * 计算原料
 */
const buildBasicMaterials = (node?: Node) => {
	if (!node) {
		node = treeRef.value!.root
		basicMaterialList.value = []
	}
	// 是叶子节点（即产线所需的原料）
	if (node.childNodes.length === 0) {
		const find = basicMaterialList.value.find(item => item.itemId === node.data.itemId)
		if (find) {
			const fraction = new Fraction(find.quantityPerMinuteExactFraction!.numerator, find.quantityPerMinuteExactFraction!.denominator).add(new Fraction(node.data.quantityPerMinuteExactFraction.numerator, node.data.quantityPerMinuteExactFraction.denominator))
			find.quantityPerMinute = Math.round(fraction.valueOf() * 100) / 100
			find.quantityPerMinuteExactFraction = {
				numerator: fraction.n,
				denominator: fraction.d
			}
		} else {
			const fraction = new Fraction(node.data.quantityPerMinuteExactFraction!.numerator, node.data.quantityPerMinuteExactFraction!.denominator)
			basicMaterialList.value.push({
				itemId: node.data.itemId,
				itemName: node.data.itemName,
				quantityPerMinute: Math.round(fraction.valueOf() * 100) / 100,
				quantityPerMinuteExactFraction: {
					numerator: fraction.n,
					denominator: fraction.d
				},
				image: node.data.image
			})
		}
	}
	// 非叶子节点
	else {
		for (const childNode of node.childNodes) {
			buildBasicMaterials(childNode)
		}
	}
}

const statisticsHandler = (node: Node) => {
	// 副产物
	if (node.data.byProductList && node.data.byProductList.length > 0) {
		for (const byProduct of node.data.byProductList) {
			const find = byProductList.value.find(item => item.itemId === byProduct.itemId)
			if (find) {
				find.quantityPerMinute = find.quantityPerMinute!.add(byProduct.quantityPerMinute)
				find.extra = find.extra!.add(byProduct.quantityPerMinute)
			} else {
				byProductList.value.push({
					itemId: byProduct.itemId,
					itemName: byProduct.itemName,
					quantityPerMinute: byProduct.quantityPerMinute,
					extra: byProduct.quantityPerMinute,
					image: byProduct.image
				})
			}
		}
	}
	// 自动化产线建筑
	if (node.data.buildingVo) {
		const link = links.value.find(link => link.from === node.id)
		if (!link) {
			throw Error('无法匹配节点')
		}
		const find = productionLineBuildingList.value.find(building => building.buildingId === link.buildingId)
		if (find) {
			find.count += link.count
			const fraction = new Fraction(find.countExactFraction!.numerator, find.countExactFraction!.denominator).add(new Fraction(link.countExactFraction.numerator, link.countExactFraction.denominator))
			find.countExactFraction = {
				numerator: fraction.n,
				denominator: fraction.d
			}
			find.countExact = Math.round(fraction.valueOf() * 100) / 100
		} else {
			const fraction = new Fraction(link.countExactFraction!.numerator, link.countExactFraction!.denominator)
			productionLineBuildingList.value.push({
				buildingId: link.buildingId,
				buildingName: link.buildingName,
				needPower: node.data.buildingVo.needPower,
				power: node.data.buildingVo.power,
				image: link.image,
				overclockable: link.overclockable,
				count: link.count,
				countExact: Math.round(fraction.valueOf() * 100) / 100,
				countExactFraction: {
					numerator: fraction.n,
					denominator: fraction.d
				}
			})
		}
	}
	// 手搓建筑
	if (node.data.handmadeBuildingVoList && node.data.handmadeBuildingVoList.length > 0) {
		for (const handmadeBuildingVo of node.data.handmadeBuildingVoList) {
			const find = productionLineBuildingList.value.find(building => building.buildingId === handmadeBuildingVo.buildingId)
			if (!find) {
				productionLineBuildingList.value.push({
					buildingId: handmadeBuildingVo.buildingId,
					buildingName: handmadeBuildingVo.buildingName,
					needPower: handmadeBuildingVo.needPower,
					power: handmadeBuildingVo.power,
					image: handmadeBuildingVo.image,
					overclockable: handmadeBuildingVo.overclockable,
					count: 1,
					countExact: 1,
					countExactFraction: {numerator: 1, denominator: 1}
				})
			}
		}
	}
	// 电力
	if (node.data.buildingVo && node.data.buildingVo.needPower === 1) {
		totalPower.value += node.data.buildingVo.power
	}
}

/*
	============================================= 数据统计 - 结束 =============================================
 */

</script>

<style lang="scss" scoped>

</style>
